/**
 * @file mpd.c
 * MPD source for TrayInfo.
 */

/* MPD support added by Laszlo Ashin <kodest@gmail.com> */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> /* close() */
#include <netdb.h>

#include "trayinfo.h"
#include "utils.h"

/** Source handle. */
static source_handle_t *mpd_sh;

/** Maximal length of the output can take. */
#define MAXLEN	128

/** Minimal value of the maxlen variable. */
#define MINLEN	8

/** Size of the receive buffer. */
#define RECVBUFSIZE	512

/** Port number of the MPD server */
static char *port;

/** Host name of the MPD server */
static char *host;

/** Password of the MPD server */
static char *pass;

/** Maximal output length defined by the user. */
static int maxlen;

/** File name of the current song. */
static char file[MAXLEN];

/** Name of the artist for the current song. */
static char artist[MAXLEN];

/** Title of the current song. */
static char title[MAXLEN];

/** Bitfield contains which data mpd server sent to us. */
static enum {
	MPD_SENT_NOTHING = 0x00,
	MPD_SENT_FILE = 0x01,
	MPD_SENT_TITLE = 0x02,
	MPD_SENT_ARTIST = 0x04,
} data_mpd_sent;

/** Not zero if MPD is playing a song right now. */
static int playing = 0;

/** Possible states of the communication between us and the MPD server. */
static enum {
	/** Connection is open and ready to send status request. */
	INIT,

	/** Status request has been sent, waiting for reply. */
	STATUS_SENT,

	/** Reply for status request has been received. */
	STATUS_CAUGHT,

	/** Song request has been sent, waiting for reply. */
	SONG_SENT,

	/** Reply for song request has been received. */
	SONG_CAUGHT
} status;

static enum {
	NO_ERROR,
	ERR_RESOLV,
	ERR_CONNECT
} conn_status;

/** Starts a query for the current song. */
static void
mpd_send_request(int fd)
{
	if (status == INIT) {
		source_send(mpd_sh, "status\n", 7);
		status = STATUS_SENT;
	} else if (status == STATUS_CAUGHT && playing) {
		data_mpd_sent = MPD_SENT_NOTHING;
		source_send(mpd_sh, "currentsong\n", 12);
		status = SONG_SENT;
	} else {
		status = INIT;
	}
}

/** Show the name of current song. */
static int
mpd_write(double now, int fd)
{
	size_t n = 0;

	if (playing) {
		static char buf[MAXLEN] = "";
		int artist_and_title = MPD_SENT_ARTIST | MPD_SENT_TITLE;

		if (maxlen > MAXLEN) {
			maxlen = MAXLEN;
		}
		if (maxlen < MINLEN) {
			maxlen = MINLEN;
		}
		if ((data_mpd_sent & (artist_and_title)) == artist_and_title) {
			size_t alen = strlen(artist);
			size_t tlen = strlen(title);
			int ashortened = 0;
			int tshortened = 0;

			while (alen + tlen + 2 > maxlen) {
				if (alen > tlen) {
					--alen;
					ashortened = !0;
				} else {
					--tlen;
					tshortened = !0;
				}
			}
			strncpy(buf, artist, alen);
			if (ashortened) {
				buf[alen - 1] = '.';
			}
			buf[alen] = '/';
			strncpy(buf + alen + 1, title, tlen);
			if (tshortened) {
				buf[alen + tlen] = '.';
			}
			n = alen + tlen + 1;
			buf[n] = '\0';
		} else if ((data_mpd_sent & MPD_SENT_FILE) == MPD_SENT_FILE) {
			n = strlen(file);

			strncpy(buf, file, maxlen - 1);
			if (n >= maxlen) {
				n = maxlen - 1;
				buf[n] = '\0';
				buf[n - 1] = '.';
				buf[n - 2] = '.';
			}
		} else if (buf[0] != '\0') {
			n = strlen(buf);
		}
		if (n > 0) {
			show("%s", buf);
		}
	}

	if ((fd != 0) && status == INIT) {
		mpd_send_request(fd);
	}

	return n;
}

/** Open a connection to the MPD server. */
static int
mpd_open(void)
{
	struct addrinfo hints;
	struct addrinfo *res, *rp;
	int fd, s;
	
	memset(&hints, 0, sizeof(hints));
	hints.ai_flags = AI_ADDRCONFIG;
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = 0;

	s = getaddrinfo(
		(host != NULL) ? host : "localhost",
		(port != NULL) ? port : "6600",
		&hints, &res
	);
	if (s) {
		if (conn_status != ERR_RESOLV) {
			show_error("mpd: getaddrinfo(): %s", gai_strerror(s));
			conn_status = ERR_RESOLV;
		}
		return 0;
	}

	for (rp = res; rp; rp = rp->ai_next) {
		fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
		if (fd == -1) {
			continue;
		}

		if (!connect(fd, rp->ai_addr, rp->ai_addrlen)) {
			break;
		}

		close(fd);
	}

	freeaddrinfo(res);

	if (!rp) {
		if (conn_status != ERR_CONNECT) {
			show_error("mpd: unable to connect");
			conn_status = ERR_CONNECT;
		}
		return 0;
	}

	if (conn_status != NO_ERROR) {
		show_error("mpd: successfully connected");
		conn_status = NO_ERROR;
	}

	if (pass && *pass) {
		char *buf;

		buf = (char *)malloc(11 + strlen(pass));
		strcpy(buf, "password ");
		strcat(buf, pass);
		strcat(buf, "\n");
		source_send(mpd_sh, buf, strlen(buf));
		free(buf);
	}
	status = INIT;
	playing = 0;
	mpd_send_request(fd);
	return fd;
}

/** Close the connection. */
static void
mpd_close(int fd)
{
	close(fd);
	playing = 0;
}

/** Parse received data. */
static void
mpd_process_recved(char *s, int fd)
{
	if (status == STATUS_SENT) {
		if (strstartswith(s, "state: ")) {
			playing = strstartswith(s + 7, "play");
		} else if (strcmp(s, "OK") == 0) {
			status = STATUS_CAUGHT;
			mpd_send_request(fd);
		}
	} else if (status == SONG_SENT) {
		if (strstartswith(s, "file: ")) {
			char *t = strrchr(s, '/');

			t = (t != NULL) ? t + 1 : s + 6;
			strncpy(file, t, MAXLEN - 1);
			file[MAXLEN - 1] = '\0';
			data_mpd_sent |= MPD_SENT_FILE;
		} else if (strstartswith(s, "Artist: ")) {
			strncpy(artist, s + 8, MAXLEN - 1);
			artist[MAXLEN - 1] = '\0';
			data_mpd_sent |= MPD_SENT_ARTIST;
		} else if (strstartswith(s, "Title: ")) {
			strncpy(title, s + 7, MAXLEN - 1);
			title[MAXLEN - 1] = '\0';
			data_mpd_sent |= MPD_SENT_TITLE;
		} else if (strcmp(s, "OK") == 0) {
			status = SONG_CAUGHT;
			mpd_send_request(fd);
		}
	}
}

/** Get data from MPD server. */
static ssize_t
mpd_read(int fd)
{
	static char buf[RECVBUFSIZE];
	static size_t sord = 0; /* size of received data */
	ssize_t n = recv(fd, buf + sord, sizeof(buf) - sord, 0);
	if (n > 0) {
		char *sep;

		sord += n;
		while ((sep = memchr(buf, '\n', sord))) {
			int l;

			*sep++ = '\0';
			l = sep - buf;
			mpd_process_recved(buf, fd);
			sord -= l;
			memmove(buf, buf + l, sord);
		}
		/* check for overflow */
		if (sord == sizeof(buf)) {
			/* simply drop everything */
			sord = 0;
		}
	}
	return n;
}

static int
mpd_init(source_handle_t *sh)
{
	mpd_sh = sh;
	port = pass = host = NULL;
	maxlen = MAXLEN;
	conn_status = NO_ERROR;
	return 0;
}

static void
mpd_done(void)
{
	if (port) free(port);
	if (pass) free(pass);
	if (host) free(host);
	port = pass = host = NULL;
}

static int
mpd_cfg_port(const char *s)
{
	if (port) free(port);
	port = strdup(s);

	return 0;
}

static int
mpd_cfg_host(const char *s)
{
	if (host) free(host);
	host = strdup(s);

	return 0;
}

static int
mpd_cfg_maxlen(const char *s)
{
	int ret = cfgstr2int(s, &maxlen);
	if (maxlen > MAXLEN) {
		show_error("mpd-maxlen was greater than %d", MAXLEN);
		maxlen = MAXLEN;
	} else if (maxlen < MINLEN) {
		show_error("mpd-maxlen was less than %d", MINLEN);
		maxlen = MINLEN;
	}
	return ret;
}

static int
mpd_cfg_pass(const char *s)
{
	if (pass) free(pass);
	pass = strdup(s);

	return 0;
}

int
init_module(module_phase_t phase)
{
	switch (phase) {
	case MODULE_PHASE_INIT: {
		source_t source = {
			.name = "mpd",
			.init = mpd_init,
			.done = mpd_done,
			.write = mpd_write,
			.open = mpd_open,
			.close = mpd_close,
			.read = mpd_read,
		};
		/* mpd_sh = */ register_source(&source);
		if (mpd_sh == NULL) {
			return !0;
		}
		register_config_handler("mpd-port", mpd_cfg_port);
		register_config_handler("mpd-host", mpd_cfg_host);
		register_config_handler("mpd-maxlen", mpd_cfg_maxlen);
		register_config_handler("mpd-pass", mpd_cfg_pass);
		break;
	}
	case MODULE_PHASE_EXIT:
		forget_config_handler("mpd-pass", mpd_cfg_pass);
		forget_config_handler("mpd-maxlen", mpd_cfg_maxlen);
		forget_config_handler("mpd-host", mpd_cfg_host);
		forget_config_handler("mpd-port", mpd_cfg_port);
		forget_source(mpd_sh);
		break;
	}
	return 0;
}
